using System.Collections;
using System.Reflection;
using GraphQL.DataLoader;

namespace GraphQL.Types;

/// <summary>
/// Parses a <see cref="System.Type"/> along with nullability information into its constituent parts
/// in preparation for creating a graph type that represents such a type.
/// </summary>
public class TypeInformation
{
    /// <summary>
    /// The member being inspected. This is a <see cref="MethodInfo"/>, <see cref="PropertyInfo"/> or <see cref="FieldInfo"/> instance.
    /// </summary>
    public MemberInfo MemberInfo { get; }

    /// <summary>
    /// The parameter being inspected. Only applicable when <see cref="MemberInfo"/> is a <see cref="MethodInfo"/>.
    /// </summary>
    public ParameterInfo? ParameterInfo { get; }

    /// <summary>
    /// Indicates that this is an input type (an argument or input field); false for output types.
    /// </summary>
    public bool IsInputType { get; }

    /// <summary>
    /// The underlying CLR type represented. This might be the underlying type of a <see cref="Nullable{T}"/>
    /// or the underlying type of a <see cref="IEnumerable{T}"/>.
    /// </summary>
    public Type Type { get; set; }

    /// <summary>
    /// Indicates if the underlying type is nullable.
    /// </summary>
    public bool IsNullable { get; set; }

    /// <summary>
    /// Indicates that this represents a list of elements.
    /// </summary>
    public bool IsList { get; set; }

    /// <summary>
    /// Indicates if the list is nullable.
    /// </summary>
    public bool ListIsNullable { get; set; }

    private Type? _graphType;
    /// <summary>
    /// The graph type of the underlying CLR type or <see langword="null"/> to detect the graph type automatically.
    /// </summary>
    public Type? GraphType
    {
        get => _graphType;
        set
        {
            if (value == null)
            {
                _graphType = null;
                return;
            }
            if (IsInputType)
            {
                if (!value.IsInputType())
                {
                    throw new ArgumentOutOfRangeException(nameof(value), "Value can only be an input graph type.");
                }
            }
            else if (!value.IsOutputType())
            {
                throw new ArgumentOutOfRangeException(nameof(value), "Value can only be an output graph type.");
            }
            if (!value.IsNamedType())
            {
                throw new ArgumentOutOfRangeException(nameof(value), "Value must be a named graph type.");
            }
            _graphType = value;
        }
    }

    /// <summary>
    /// Initializes an instance with the specified properties.
    /// </summary>
    /// <param name="memberInfo">The member being inspected.</param>
    /// <param name="isInputType">Indicates that this is an input type (an argument or input field); false for output types.</param>
    /// <param name="type">The underlying type.</param>
    /// <param name="isNullable">Indicates that the underlying type is nullable.</param>
    /// <param name="isList">Indicates that this member represents a list of elements.</param>
    /// <param name="listIsNullable">Indicates that the list is nullable.</param>
    /// <param name="graphType">The graph type of the underlying CLR type; null to be generated by <see cref="GraphQL.TypeExtensions.GetGraphTypeFromType(System.Type, bool, TypeMappingMode)"/>.</param>
    public TypeInformation(MemberInfo memberInfo, bool isInputType, Type type, bool isNullable, bool isList, bool listIsNullable, Type? graphType)
    {
        MemberInfo = memberInfo ?? throw new ArgumentNullException(nameof(memberInfo));
        IsInputType = isInputType;
        Type = type ?? throw new ArgumentNullException(nameof(type));
        IsNullable = isNullable;
        IsList = isList;
        ListIsNullable = listIsNullable;
        GraphType = graphType;
    }

    /// <summary>
    /// Initializes an instance with the specified properties.
    /// </summary>
    /// <param name="parameterInfo">The parameter being inspected.</param>
    /// <param name="type">The underlying type.</param>
    /// <param name="isNullable">Indicates that the underlying type is nullable.</param>
    /// <param name="isList">Indicates that this member represents a list of elements.</param>
    /// <param name="listIsNullable">Indicates that the list is nullable.</param>
    /// <param name="graphType">The graph type of the underlying CLR type; null to be generated by <see cref="GraphQL.TypeExtensions.GetGraphTypeFromType(System.Type, bool, TypeMappingMode)"/>.</param>
    public TypeInformation(ParameterInfo parameterInfo, Type type, bool isNullable, bool isList, bool listIsNullable, Type? graphType)
        : this(parameterInfo.Member, true, type, isNullable, isList, listIsNullable, graphType)
    {
        ParameterInfo = parameterInfo;
    }

    /// <summary>
    /// Initializes an instance containing type information necessary to select a graph type.
    /// The instance is populated based on inspecting the type and NRT annotations on the specified property.
    /// </summary>
    public TypeInformation(PropertyInfo propertyInfo, bool isInput)
        : this(propertyInfo, isInput, propertyInfo.PropertyType, false, false, false, null)
    {
        // for the purposes of processing the nullability information, if the property is
        // read-only, read the nullability information as if it were an output type (so read the getter)
        var treatAsInput = isInput && propertyInfo.CanWrite;
        var typeTree = Interpret(new NullabilityInfoContext().Create(propertyInfo), treatAsInput);

        ProcessTypeTree(typeTree, isInput);
    }

    /// <summary>
    /// Initializes an instance containing type information necessary to select a graph type.
    /// The instance is populated based on inspecting the type and NRT annotations on the specified field.
    /// </summary>
    public TypeInformation(FieldInfo fieldInfo, bool isInput)
        : this(fieldInfo, isInput, fieldInfo.FieldType, false, false, false, null)
    {
        var typeTree = Interpret(new NullabilityInfoContext().Create(fieldInfo), isInput);

        ProcessTypeTree(typeTree, isInput);
    }

    /// <summary>
    /// Initializes an instance containing type information necessary to select a graph type.
    /// The instance is populated based on inspecting the type and NRT annotations on the specified method.
    /// </summary>
    public TypeInformation(MethodInfo methodInfo)
        : this(methodInfo, false, methodInfo.ReturnType, false, false, false, null)
    {
        var typeTree = Interpret(new NullabilityInfoContext().Create(methodInfo.ReturnParameter), false);

        ProcessTypeTree(typeTree, false);
    }

    /// <summary>
    /// Initializes an instance containing type information necessary to select a graph type.
    /// The instance is populated based on inspecting the type and NRT annotations on the specified parameter.
    /// </summary>
    public TypeInformation(ParameterInfo parameterInfo)
        : this(parameterInfo, parameterInfo.ParameterType, false, false, false, null)
    {
        var typeTree = Interpret(new NullabilityInfoContext().Create(parameterInfo), true);

        ProcessTypeTree(typeTree, true);
    }

    /// <summary>
    /// Populates the <see cref="Type"/>, <see cref="IsNullable"/>, <see cref="IsList"/> and <see cref="ListIsNullable"/>
    /// properties of this instance from a provided <paramref name="typeTree"/>.
    /// </summary>
    private void ProcessTypeTree(List<(Type Type, NullabilityState Nullable)> typeTree, bool isInput)
    {
        foreach (var type in typeTree)
        {
            //detect list types, but not lists of lists
            if (!IsList)
            {
                if (type.Type.IsArray)
                {
                    //unwrap type and mark as list
                    IsList = true;
                    ListIsNullable = IsNullable || type.Nullable != NullabilityState.NotNull;
                    continue;
                }
                if (type.Type.IsGenericType)
                {
                    if (IsRecognizedListType(type.Type))
                    {
                        //unwrap type and mark as list
                        IsList = true;
                        ListIsNullable = IsNullable || type.Nullable != NullabilityState.NotNull;
                        IsNullable = false;
                        continue;
                    }
                }
                if (type.Type == typeof(IEnumerable) || type.Type == typeof(ICollection))
                {
                    //assume list of nullable object
                    IsList = true;
                    ListIsNullable = IsNullable || type.Nullable != NullabilityState.NotNull;
                    IsNullable = false;
                    break;
                }
            }
            if (!isInput)
            {
                if (type.Type.IsGenericType)
                {
                    var genericType = type.Type.GetGenericTypeDefinition();
                    if (genericType == typeof(Task<>) || genericType == typeof(ValueTask<>)
                        || genericType == typeof(IAsyncEnumerable<>)
                        || genericType == typeof(IDataLoaderResult<>) || genericType == typeof(IObservable<>))
                    {
                        //unwrap type
                        IsNullable |= type.Nullable != NullabilityState.NotNull;
                        continue;
                    }
                }
                if (type.Type == typeof(IDataLoaderResult))
                {
                    //assume nullable object
                    break;
                }
            }
            //found match
            IsNullable |= type.Nullable != NullabilityState.NotNull;
            Type = type.Type;
            return;
        }
        //unknown type
        IsNullable = true;
        Type = typeof(object);
    }

    /// <summary>
    /// Flattens a complex <see cref="NullabilityInfo"/> structure into a list of types and nullability flags.
    /// <see cref="Nullable{T}"/> structs return their underlying type rather than <see cref="Nullable{T}"/>.
    /// </summary>
    private static List<(Type Type, NullabilityState Nullable)> Interpret(NullabilityInfo info, bool isInput)
    {
        var list = new List<(Type, NullabilityState)>(info.GenericTypeArguments.Length + 1);
        RecursiveLoop(info);
        return list;

        void RecursiveLoop(NullabilityInfo info)
        {
            if (info.Type.IsGenericType)
            {
                var nullableType = Nullable.GetUnderlyingType(info.Type);
                if (nullableType != null)
                {
                    list.Add((nullableType, NullabilityState.Nullable));
                }
                else
                {
                    list.Add((info.Type, isInput ? info.WriteState : info.ReadState));
                }
                foreach (var t in info.GenericTypeArguments)
                {
                    RecursiveLoop(t);
                }
            }
            else if (info.ElementType != null)
            {
                list.Add((info.Type, isInput ? info.WriteState : info.ReadState));
                RecursiveLoop(info.ElementType);
            }
            else
            {
                list.Add((info.Type, isInput ? info.WriteState : info.ReadState));
            }
        }
    }

    /// <summary>
    /// Applies <see cref="GraphQLAttribute"/> attributes for the specified member to this instance.
    /// Also scans the member's owning module and assembly for globally-applied attributes,
    /// and applies attributes defined within <see cref="GlobalSwitches.GlobalAttributes"/>.
    /// </summary>
    public virtual void ApplyAttributes()
    {
        var memberOrParameter = (ICustomAttributeProvider?)ParameterInfo ?? MemberInfo;
        if (memberOrParameter.IsDefined(typeof(System.ComponentModel.DataAnnotations.RequiredAttribute), false))
        {
            if (IsList)
            {
                ListIsNullable = false;
            }
            else
            {
                IsNullable = false;
            }
        }

        var attributes = ParameterInfo != null
            ? ParameterInfo.GetGraphQLAttributes()
            : MemberInfo.GetGraphQLAttributes();
        foreach (var attr in attributes)
        {
            attr.Modify(this);
        }
    }

    /// <summary>
    /// Returns a graph type constructed based on the properties set within this instance.
    /// If <see cref="GraphType"/> is <see langword="null"/>, the graph type is generated via
    /// <see cref="GraphQL.TypeExtensions.GetGraphTypeFromType(System.Type, bool, TypeMappingMode)"/>.
    /// The graph type is then wrapped with <see cref="NonNullGraphType{T}"/> and/or
    /// <see cref="ListGraphType{T}"/> as appropriate.
    /// </summary>
    public virtual Type ConstructGraphType()
    {
        var type = GraphType;
        if (type != null)
        {
            if (!IsNullable)
                type = typeof(NonNullGraphType<>).MakeGenericType(type);
        }
        else
        {
            type = Type.GetGraphTypeFromType(IsNullable, IsInputType ? TypeMappingMode.InputType : TypeMappingMode.OutputType);
        }
        if (IsList)
        {
            type = typeof(ListGraphType<>).MakeGenericType(type);
            if (!ListIsNullable)
                type = typeof(NonNullGraphType<>).MakeGenericType(type);
        }
        return type;
    }

    internal static readonly Type[] EnumerableListTypes = new Type[] {
        typeof(IEnumerable<>),
        typeof(IList<>),
        typeof(List<>),
        typeof(ICollection<>),
        typeof(IReadOnlyCollection<>),
        typeof(IReadOnlyList<>),
        typeof(HashSet<>),
        typeof(ISet<>),
    };

    /// <summary>
    /// Determines if the specified type is one of a certain set of recognized generic list types.
    /// Does not match for <see cref="string"/> or <see cref="IDictionary{TKey, TValue}"/> or other
    /// types which may also be able to be cast to <see cref="IEnumerable{T}"/>.
    /// </summary>
    private bool IsRecognizedListType(Type type)
        => Array.IndexOf(EnumerableListTypes, type.GetGenericTypeDefinition()) >= 0;
}
